﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using Microsoft.Data.Schema.ScriptDom;
using Microsoft.Data.Schema.ScriptDom.Sql;
using PropertyExpression.Common;

namespace PropertyExpression.ControllableQuery.Toolkit
{
    public static class QueryChecker
    {
        public static void CheckNonQueries(string connectionString, params Func<SqlCommand, INonQuery>[] queryFuncs)
        {
            CheckNonQueries(
                connectionString, new DoNothingQRepository(),
                queryFuncs.Select(func => Func.New(
                    (SqlCommand context) => new NonQueryHolder(func(context), _ => new QLocation.NonDisplay(func.Method)))).ToArray());
        }

        private static void CheckNonQueries(string connectionString, IQRepository qRepository, params Func<SqlCommand, NonQueryHolder>[] queryHolderFuncs)
        {
            queryHolderFuncs.ForEach(
                queryHolderFunc =>
                ExecuteQuery(connectionString, _ => queryHolderFunc(_).ToQueryHolder(), delegate { return new { }; }, false, qRepository));
        }

        private class NonQueryHolder
        {
            public readonly INonQuery NonQuery;
            public readonly Func<IQuery, QLocation> QLocationFunc;

            public NonQueryHolder(INonQuery nonQuery, Func<IQuery, QLocation> qLocationFunc)
            {
                NonQuery = nonQuery;
                QLocationFunc = qLocationFunc;
            }
        }

        public static void CheckQueries<T>(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, params Func<SqlCommand, IQuery<T>>[] queryFuncs)
        {
            CheckQueries(
                connectionString,
                queryResultPropertyTypeChecker,
                new DoNothingQRepository(),
                queryFuncs.Select(func => Func.New((SqlCommand context) => new QueryHolder<T>(func(context), _ => new QLocation.NonDisplay(func.Method)))).ToArray());
        }

        private class QueryHolder<T>
        {
            public readonly IQuery<T> Query;
            public readonly Func<IQuery, QLocation> QLocationFunc;

            public QueryHolder(IQuery<T> query, Func<IQuery, QLocation> qLocationFunc)
            {
                Query = query;
                QLocationFunc = qLocationFunc;
            }
        }

        private class QueryHolder
        {
            public readonly IQuery Query;
            public readonly Func<IQuery, QLocation> QLocationFunc;

            public QueryHolder(IQuery query, Func<IQuery, QLocation> qLocationFunc)
            {
                Query = query;
                QLocationFunc = qLocationFunc;
            }
        }

        private static QueryHolder ToQueryHolder<T>(this QueryHolder<T> it)
        {
            return new QueryHolder(it.Query, it.QLocationFunc);
        }

        private static QueryHolder ToQueryHolder(this NonQueryHolder it)
        {
            return new QueryHolder(it.NonQuery, it.QLocationFunc);
        }

        private static QLocation GetQLocation(this QueryHolder it)
        {
            return it.QLocationFunc(it.Query);
        }

        private static void CheckQueries<T>(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, IQRepository qRepository, params Func<SqlCommand, QueryHolder<T>>[] queryHolderFuncs)
        {
            queryHolderFuncs.ForEach(
                queryHolderFunc => {
                    var dataInfoPropertyInfos = typeof (T).GetProperties();
                    var dataInfoPropertyInfoDictionary = dataInfoPropertyInfos.ToDictionary(
                        propertyInfo => propertyInfo.Name);
                    var executeResult = ExecuteQuery(
                        connectionString, _ => queryHolderFunc(_).ToQueryHolder(),
                        (sqlDataReader, queryHolder) => new { schemaTable = sqlDataReader.GetSchemaTable(), queryHolder }, true, qRepository);
                    executeResult.schemaTable.Rows.Cast<DataRow>().ForEach(
                        dataRow => dataInfoPropertyInfoDictionary.GetOptionalValue(dataRow.GetColumnName()).Process(
                            propertyInfo => queryResultPropertyTypeChecker(
                                propertyInfo, dataRow, executeResult.queryHolder.GetQLocation()),
                            () => {
                                throw new QueryCheckException(
                                    string.Format(
                                        @"The query result type does not contain a definition for property '{0}'.{1}",
                                        dataRow.GetColumnName(),
                                        executeResult.queryHolder.GetQLocation().Display()));
                            }));
                    var schemaTableDictionary = executeResult.schemaTable.Rows.Cast<DataRow>().ToDictionary(_ => _.GetColumnName());
                    foreach (var propertyInfo in dataInfoPropertyInfos)
                    {
                        if (!schemaTableDictionary.GetOptionalValue(propertyInfo.Name).HasValue)
                        {
                            throw new QueryCheckException(
                                string.Format(
                                    @"The query result does not contain a column '{0}'.{1}",
                                    propertyInfo.Name,
                                    executeResult.queryHolder.GetQLocation().Display()));
                        }
                    }
                });
        }

        private static TResult ExecuteQuery<TResult>(string connectionString, Func<SqlCommand, QueryHolder> queryHolderFunc, Func<SqlDataReader, QueryHolder, TResult> resultFunc, bool doubleCheck, IQRepository qRepository)
        {
            return connectionString.WithSqlConnection(
                connection => connection.WithSqlCommand(
                    sqlCommand => {
                        var queryHolder = queryHolderFunc(sqlCommand);
                        var queryText = queryHolder.Query.TransformText();
                        sqlCommand.CommandText = doubleCheck ? InsertFalseCondition(queryText) : queryText;

                        SqlDataReader sqlDataReader;
                        try
                        {
                            sqlDataReader = sqlCommand.ExecuteReader(CommandBehavior.SchemaOnly);
                        }
                        catch (SqlException sqlException)
                        {
                            if (sqlCommand.CommandText != queryText)
                            {
                                sqlCommand.CommandText = queryText;
                                SqlDataReader sqlDataReader2;
                                try
                                {
                                    sqlDataReader2 = sqlCommand.ExecuteReader(CommandBehavior.SchemaOnly);
                                }
                                catch (SqlException sqlException2)
                                {
                                    throw new QueryCheckException(sqlCommand.CommandText, sqlException2, queryHolder.GetQLocation());
                                }
                                using (sqlDataReader2)
                                {
                                    qRepository.Add(sqlCommand, queryHolder.GetQLocation());
                                    return resultFunc(sqlDataReader2, queryHolder);
                                }
                            }
                            throw new QueryCheckException(sqlCommand.CommandText, sqlException, queryHolder.GetQLocation());
                        }
                        using (sqlDataReader)
                        {
                            qRepository.Add(sqlCommand, queryHolder.GetQLocation());
                            return resultFunc(sqlDataReader, queryHolder);
                        }
                    })
                );
        }

        private static string InsertFalseCondition(string queryText)
        {
            var parser = new TSql100Parser(false);
            IList<ParseError> parseErrors;
            var scriptFragment = parser.Parse(new StringReader(queryText), out parseErrors);
            if (!(scriptFragment is TSqlScript))
                return queryText;
            var actions = new List<Action<StringBuilder>>();
            foreach (var sqlBatch in ((TSqlScript) scriptFragment).Batches)
            {
                foreach (var sqlStatement in sqlBatch.Statements)
                {
                    if (!(sqlStatement is SelectStatement)) continue;
                    var selectStatement = ((SelectStatement) sqlStatement).QueryExpression;
                    if (!(selectStatement is QuerySpecification)) continue;
                    var querySpecification = (QuerySpecification) selectStatement;
                    var whereClause = querySpecification.WhereClause;
                    if (whereClause == null)
                    {
                        var fromClause = querySpecification.FromClauses.LastOrDefault();
                        if (fromClause == null)
                            continue;
                        actions.Add(builder => builder.Insert(
                            fromClause.StartOffset + fromClause.FragmentLength, " WHERE 0 = 1 "));
                    }
                    else
                    {
                        var sqlParserToken = whereClause.ScriptTokenStream[whereClause.FirstTokenIndex];
                        if (sqlParserToken.TokenType != TSqlTokenType.Where) continue;

                        actions.Add(
                            builder => {
                                builder.Insert(
                                    whereClause.StartOffset + whereClause.FragmentLength, " ) ");
                                builder.Insert(
                                    sqlParserToken.Offset + sqlParserToken.Text.Length, " 0 = 1 AND ( ");
                            });
                    }
                }
            }
            actions.Reverse();
            var stringBuilder = new StringBuilder(queryText);
            foreach (var action in actions) action(stringBuilder);
            return stringBuilder.ToString();
        }

        private static TResult WithSqlConnection<TResult>(this string connectionString, Func<SqlConnection, TResult> func)
        {
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();

                return func(connection);
            }
        }

        internal static IEnumerable<TResult> WithSqlConnection<TResult>(this string connectionString, Func<SqlConnection, IEnumerable<TResult>> func)
        {
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();

                foreach (var result in func(connection)) yield return result;
            }
        }

        internal static TResult WithSqlCommand<TResult>(this SqlConnection connection, Func<SqlCommand, TResult> func)
        {
            using (var sqlCommand = new SqlCommand())
            {
                sqlCommand.Connection = connection;

                return func(sqlCommand);
            }
        }

        private const BindingFlags allBindingFlags = BindingFlags.Default |
                                                     BindingFlags.IgnoreCase |
                                                     BindingFlags.DeclaredOnly |
                                                     BindingFlags.Instance |
                                                     BindingFlags.Static |
                                                     BindingFlags.Public |
                                                     BindingFlags.NonPublic |
                                                     BindingFlags.FlattenHierarchy |
                                                     BindingFlags.InvokeMethod |
                                                     BindingFlags.CreateInstance |
                                                     BindingFlags.GetField |
                                                     BindingFlags.SetField |
                                                     BindingFlags.GetProperty |
                                                     BindingFlags.SetProperty |
                                                     BindingFlags.PutDispProperty |
                                                     BindingFlags.PutRefDispProperty |
                                                     BindingFlags.ExactBinding |
                                                     BindingFlags.SuppressChangeType |
                                                     BindingFlags.OptionalParamBinding |
                                                     BindingFlags.IgnoreReturn;

        public static void CheckAllQueriesInAvailableAssemblies(
            string connectionString,
            QueryResultPropertyTypeChecker queryResultPropertyTypeChecker,
            ParamCreator paramCreator,
            Func<Type, bool> queryTypePredicate,
            Func<MethodInfo, bool> methodInfoPredicate,
            ChoiceInvocationGetter choiceInvocationGetter)
        {
            CheckAllQueries(
                connectionString,
                queryResultPropertyTypeChecker,
                paramCreator,
                queryTypePredicate,
                methodInfoPredicate,
                choiceInvocationGetter,
                GetAllAssemblies().SelectMany(assembly => assembly.GetTypes()),
                new DoNothingQRepository());
        }

        public static void CheckAllQueries(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator, Func<Type, bool> queryTypePredicate, Func<MethodInfo, bool> methodInfoPredicate, ChoiceInvocationGetter choiceInvocationGetter, IEnumerable<Type> types, IQRepository qRepository)
        {
            foreach (var type in types)
            {
                if (!QueryExtensions.QueryImplementationType.Equals(type)
                    && !QueryExtensions.NonQueryImplementationType.Equals(type))
                    foreach (var @interface in type.GetInterfaces())
                        if (@interface.IsGenericTypeAndEquals(typeof (IQuery<>)))
                            if (queryTypePredicate(type))
                                foreach (var queryCreator in new ChoiceInvocationContext(choiceInvocationGetter).GetConstructorInvocations(type))
                                    InvokeCheckQuery(connectionString, queryResultPropertyTypeChecker, paramCreator, queryCreator, @interface.GetGenericArguments(), query => new QLocation.Class(query), qRepository);
                            else if (typeof (INonQuery).Equals(@interface))
                                if (queryTypePredicate(type))
                                    foreach (var queryCreator in new ChoiceInvocationContext(choiceInvocationGetter).GetConstructorInvocations(type))
                                        CheckNonQuery(connectionString, paramCreator, queryCreator, query => new QLocation.Class(query), qRepository);
                type.GetMethods(allBindingFlags).ForEach(
                    methodInfo => {
                        if (!methodInfoPredicate(methodInfo)) return;
                        Action<MethodInfo> action =
                            resolveMethodInfo =>
                            CheckMethod(methodInfo, choiceInvocationGetter,
                                        queryCreator =>
                                        InvokeCheckQuery(connectionString, queryResultPropertyTypeChecker,
                                                         paramCreator, queryCreator,
                                                         resolveMethodInfo.GetGenericArguments(),
                                                         query => new QLocation.Method(methodInfo), qRepository));
                        FindMethod(
                            methodInfo,
                            methodInfo.GetGenericArguments,
                            action,
                            resolveMethodInfo => CheckMethod(
                                methodInfo, choiceInvocationGetter,
                                queryCreator => CheckNonQuery(connectionString, paramCreator, queryCreator, query => new QLocation.Method(methodInfo), qRepository)),
                            action,
                            action);
                    });
                type.GetConstructors(allBindingFlags).ForEach(
                    methodInfo => FindMethod(
                        methodInfo,
                        () => new Type[] {},
                        resolveMethodInfo => ThrowQueryCheckException(queryMethodInfo, methodInfo),
                        resolveMethodInfo => ThrowQueryCheckException(nonQueryMethodInfo, methodInfo),
                        resolveMethodInfo => ThrowQueryCheckException(queryRecordExampleMethodInfo, methodInfo),
                        resolveMethodInfo => ThrowQueryCheckException(selectMethodInfo, methodInfo)));
            }
        }

        private static void CheckMethod(
            MethodInfo methodInfo,
            ChoiceInvocationGetter choiceInvocationGetter,
            Action<QueryCreator> action)
        {
            if (!methodInfo.IsStatic)
            {
                throw new QueryCheckException(string.Format(
                    "Method is not static: '{0}'.",
                    QLocationExtensions.GetQLocationString(methodInfo)));
            }
            foreach (var queryCreator in new ChoiceInvocationContext(choiceInvocationGetter)
                .GetStaticMethodInvocations(methodInfo))
            {
                action(queryCreator);
            }
        }

        private static void ThrowQueryCheckException(MethodInfo methodInfo, ConstructorInfo constructorInfo)
        {
            throw new QueryCheckException(
                string.Format("{0} method is called in a constructor of type '{1}'.",
                              methodInfo.Name, constructorInfo.DeclaringType));
        }

        private static void InvokeCheckQuery(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator, QueryCreator queryCreator, Type[] getGenericArguments, Func<IQuery, QLocation> qLocationFunc, IQRepository qRepository)
        {
            ((IInvoker) typeof (CheckQueryInvoker<>).MakeGenericType(getGenericArguments).GetConstructors().Single()
                            .Invoke(new object[] { connectionString, queryResultPropertyTypeChecker, paramCreator, queryCreator, qLocationFunc, qRepository }))
                .Invoke();
        }

        private class CheckQueryInvoker<T> : IInvoker
        {
            private readonly string connectionString;
            private readonly QueryResultPropertyTypeChecker queryResultPropertyTypeChecker;
            private readonly ParamCreator paramCreator;
            private readonly QueryCreator queryCreator;
            private readonly Func<IQuery, QLocation> qLocationFunc;
            private readonly IQRepository qRepository;

            public CheckQueryInvoker(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator, QueryCreator queryCreator, Func<IQuery, QLocation> qLocationFunc, IQRepository qRepository)
            {
                this.connectionString = connectionString;
                this.queryResultPropertyTypeChecker = queryResultPropertyTypeChecker;
                this.paramCreator = paramCreator;
                this.queryCreator = queryCreator;
                this.qLocationFunc = qLocationFunc;
                this.qRepository = qRepository;
            }

            public void Invoke()
            {
                CheckQuery<T>(connectionString, queryResultPropertyTypeChecker, paramCreator, queryCreator, qLocationFunc, qRepository);
            }
        }

        private interface IInvoker
        {
            void Invoke();
        }

        private static readonly MethodInfo nonQueryMethodInfo =
            Util.GetMethodInfo<Func<Q, INonQuery>>(_ => _.NonQuery());

        private static readonly MethodInfo queryMethodInfo =
            Util.GetMethodInfo<Func<Q, IQuery<object>>>(_ => _.Query<object>()).GetGenericMethodDefinition();

        private static readonly MethodInfo queryRecordExampleMethodInfo =
            Util.GetMethodInfo<Func<Q, object, IQuery<object>>>((x1, x2) => x1.Query(x2)).GetGenericMethodDefinition();

        private static readonly MethodInfo selectMethodInfo =
            Util.GetMethodInfo<Func<Q, Func<ColumnSelector, object>, IQuery<object>>>((x1, x2) => x1.Select(x2)).GetGenericMethodDefinition();

        private static void CheckNonQuery(string connectionString, ParamCreator paramCreator, QueryCreator queryCreator, Func<IQuery, QLocation> qLocationFunc, IQRepository qRepository)
        {
            CheckNonQueries(connectionString, qRepository, context => new NonQueryHolder(GetQueryFuncs<INonQuery>(queryCreator, paramCreator)(context), qLocationFunc));
        }

        private static void CheckQuery<T>(string connectionString, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator, QueryCreator queryCreator, Func<IQuery, QLocation> qLocationFunc, IQRepository qRepository)
        {
            CheckQueries(connectionString, queryResultPropertyTypeChecker, qRepository, context => new QueryHolder<T>(GetQueryFuncs<IQuery<T>>(queryCreator, paramCreator)(context), qLocationFunc));
        }

        private static Func<SqlCommand, T> GetQueryFuncs<T>(QueryCreator queryCreator, ParamCreator paramCreator)
        {
            return queryContext => (T) queryCreator(
                new QueryCreatorContext(type => paramCreator(type, queryContext), queryContext));
        }

        private static IEnumerable<IEnumerable<TResult>> GetAllChoices<T, TResult>(
            IEnumerable<T> items,
            Func<T, IEnumerable<TResult>> choiceFunc)
        {
            return items.Aggregate(
                new IEnumerable<TResult>[] {},
                (seed, item) =>
                choiceFunc(item).SelectMany(
                    choiceItem => seed.Length == 0
                                      ? new[] {new[] {choiceItem}}
                                      : seed.Select(resultItem => resultItem.Concat(new[] {choiceItem}))
                    ).ToArray()
                );
        }

        private class ChoiceInvocationContext : IChoiceInvocationContext
        {
            private readonly ChoiceInvocationGetter choiceInvocationGetter;

            internal ChoiceInvocationContext(
                ChoiceInvocationGetter choiceInvocationGetter)
            {
                this.choiceInvocationGetter = choiceInvocationGetter;
            }

            public IEnumerable<QueryCreator> GetMethodBaseInvocations(MethodBase methodBase, Func<object[], object> invokeFunc)
            {
                var parameterInfos = methodBase.GetParameters();
                if (parameterInfos.Length == 0)
                {
                    return new QueryCreator[] {delegate { return invokeFunc(new object[] {}); }};
                }
                return GetAllChoices(parameterInfos, parameterInfo => GetChoiceInvocations(parameterInfo.ParameterType))
                    .Select<IEnumerable<QueryCreator>, QueryCreator>(
                        queryCreatorEnumerable => queryCreatorContext => invokeFunc(
                            queryCreatorEnumerable.Select(queryCreator => queryCreator(queryCreatorContext)).ToArray())
                    );
            }

            private IEnumerable<QueryCreator> GetChoiceInvocations(Type type)
            {
                if (type.IsGenericTypeAndEquals(typeof (IOption<>)))
                {
                    return new[] {asOptionalValueMethodInfo, nothingOptionalValueMethodInfo}.SelectMany(
                        genericMethodInfo => this.GetStaticMethodInvocations(genericMethodInfo.MakeGenericMethod(type.GetGenericArguments())));
                }
                else if (type.IsGenericTypeAndEquals(typeof (IParam<>)))
                {
                    return new QueryCreator[] {
                                                  queryCreatorContext => queryCreatorContext.CreateParamFunc(type)
                                              };
                }
                else if (typeof (bool).Equals(type))
                {
                    return new QueryCreator[] {delegate { return false; }, delegate { return true; }};
                }
                else if (typeof(SqlCommand).Equals(type))
                {
                    return new QueryCreator[] { queryCreatorContext => queryCreatorContext.QueryContext };
                }
                else
                {
                    var genericArguments = type.GetGenericArguments();
                    if (type.IsGenericTypeAndEquals(typeof (Func<>)) && genericArguments.Length == 1)
                    {
                        var genericArgument = genericArguments[0];
                        var genericMethod = getFuncQueryCreatorMethodInfo.MakeGenericMethod(genericArgument);
                        return GetChoiceInvocations(genericArgument)
                            .Select(queryCreator => (QueryCreator) genericMethod.Invoke(
                                null, new object[] {queryCreator}));
                    }
                    else if (type.IsEnum)
                    {
                        return type.GetEnumValues().Cast<object>()
                            .Select<object, QueryCreator>(enumItem => delegate { return enumItem; });
                    }
                    else if (DynamicMaterializer.IsAnonymousType(type))
                    {
                        return this.GetConstructorInvocations(type);
                    }
                    else
                    {
                        return choiceInvocationGetter(type, this);
                    }
                }
            }
        }

        private static readonly MethodInfo getFuncQueryCreatorMethodInfo =
            Util.GetMethodInfo<Func<QueryCreator, QueryCreator>>(_ => GetFuncQueryCreator<object>(_)).GetGenericMethodDefinition();

        private static QueryCreator GetFuncQueryCreator<T>(QueryCreator queryCreator)
        {
            return context => Func.New(() => (T) queryCreator(context));
        }

        private static readonly MethodInfo asOptionalValueMethodInfo =
            Util.GetMethodInfo<Func<object, IOption<object>>>(_ => _.AsOption()).GetGenericMethodDefinition();

        private static readonly MethodInfo nothingOptionalValueMethodInfo =
            Util.GetMethodInfo<Func<IOption<object>>>(() => Option.Nothing<object>()).GetGenericMethodDefinition();

        public static IEnumerable<Type> GetClassImplementations(Type classType, IEnumerable<Type> types)
        {
            return types.Where(classType.IsAssignableFrom);
        }

        public static IEnumerable<Type> GetInterfacesImplementations(Type interfaceType, IEnumerable<Type> types)
        {
            foreach (var type in types)
            {
                foreach (var @interface in type.GetInterfaces())
                {
                    if (interfaceType.IsGenericType)
                    {
                        if (@interface.IsGenericType)
                        {
                            if (interfaceType.GetGenericTypeDefinition().Equals(@interface.GetGenericTypeDefinition()))
                            {
                                var typeArguments = type.GetGenericArguments();
                                var dictionary = typeArguments.Select((type1, index) => new {type = type1, index})
                                    .ToDictionary(arg => arg.type, arg => arg.index);
                                var interfaceArguments = @interface.GetGenericArguments();
                                var inputInterfaceArguments = interfaceType.GetGenericArguments();
                                for (var index = 0; index < interfaceArguments.Length; index++)
                                {
                                    typeArguments[dictionary[interfaceArguments[index]]] = inputInterfaceArguments[index];
                                }
                                yield return type.MakeGenericType(typeArguments);
                            }
                        }
                    }
                    else
                    {
                        if (interfaceType.Equals(@interface))
                        {
                            yield return type;
                        }
                    }
                }
            }
        }

        private static IEnumerable<Assembly> GetAllAssemblies()
        {
            var assemblies = new HashSet<Assembly>();
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                assemblies.AddExcludeGlobalAssemblyCache(assembly);
                LoadReferencedAssemblies(assembly, assemblies);
            }
            return assemblies;
        }

        private static bool AddExcludeGlobalAssemblyCache(this HashSet<Assembly> assemblies, Assembly assembly)
        {
            if (assembly.GlobalAssemblyCache) return false;
            return assemblies.Add(assembly);
        }

        private static void LoadReferencedAssemblies(Assembly assembly, HashSet<Assembly> assemblies)
        {
            foreach (var assemblyName in assembly.GetReferencedAssemblies())
            {
                var loadedAssembly = Assembly.Load(assemblyName.FullName);
                if (assemblies.AddExcludeGlobalAssemblyCache(loadedAssembly))
                {
                    LoadReferencedAssemblies(loadedAssembly, assemblies);
                }
            }
        }

        private static void FindMethod(
            MethodBase methodInfo,
            Func<Type[]> genericMethodArgumentsFunc,
            Action<MethodInfo> query,
            Action<MethodInfo> nonQuery,
            Action<MethodInfo> queryRecordExample,
            Action<MethodInfo> @select)
        {
            var methodBody = methodInfo.GetMethodBody();
            if (methodBody == null) return;
            var ilAsByteArray = methodBody.GetILAsByteArray();
            var module = methodInfo.Module;
            Action onResultAction = () => { };
            ProcessMethodBody(
                ilAsByteArray,
                inlineMethod:
                    (metadataToken, position) => {
                        var resolveMember = module.ResolveMember(
                            metadataToken,
                            methodInfo.DeclaringType.GetGenericArguments(),
                            genericMethodArgumentsFunc());
                        switch (resolveMember.MemberType)
                        {
                            case MemberTypes.Method:
                                var resolveMethodInfo = (MethodInfo) resolveMember;
                                if (resolveMethodInfo.IsGenericMethod &&
                                    queryMethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => query(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                else if (resolveMethodInfo.IsGenericMethod &&
                                         queryRecordExampleMethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => queryRecordExample(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                else if (resolveMethodInfo.IsGenericMethod &&
                                         selectMethodInfo.Equals(resolveMethodInfo.GetGenericMethodDefinition()))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => @select(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                else if (nonQueryMethodInfo.Equals(resolveMethodInfo))
                                {
                                    onResultAction = () => OnQueryMethod(
                                        methodInfo, position, () => nonQuery(resolveMethodInfo), genericMethodArgumentsFunc, ilAsByteArray, module);
                                    return true;
                                }
                                break;
                        }
                        return false;
                    },
                inlineField: delegate { },
                inlineTok: delegate { },
                inlineType: delegate { });
            onResultAction();
        }

        private static void OnQueryMethod(
            MethodBase methodInfo,
            int position,
            Action action,
            Func<Type[]> genericMethodArgumentsFunc,
            byte[] ilAsByteArray,
            Module module)
        {
            ProcessMethodBody(
                ilAsByteArray,
                inlineMethod:
                    (metadataToken, position2) => {
                        if (position != position2)
                        {
                            CheckMethod(
                                methodInfo,
                                module.ResolveMember(
                                    metadataToken,
                                    methodInfo.DeclaringType.GetGenericArguments(),
                                    genericMethodArgumentsFunc()));
                        }
                        return false;
                    },
                inlineField:
                    metadataToken => CheckType(
                        methodInfo,
                        module.ResolveField(
                            metadataToken,
                            methodInfo.DeclaringType.GetGenericArguments(),
                            genericMethodArgumentsFunc()).FieldType),
                inlineTok:
                    metadataToken => {
                        Type resolveType;
                        try
                        {
                            resolveType = module.ResolveType(
                                metadataToken,
                                methodInfo.DeclaringType.GetGenericArguments(),
                                genericMethodArgumentsFunc());
                        }
                        catch
                        {
                            FieldInfo resolveField;
                            try
                            {
                                resolveField = module.ResolveField(
                                    metadataToken,
                                    methodInfo.DeclaringType.GetGenericArguments(),
                                    genericMethodArgumentsFunc());
                            }
                            catch
                            {
                                MethodBase resolveMember;
                                try
                                {
                                    resolveMember = module.ResolveMethod(
                                        metadataToken,
                                        methodInfo.DeclaringType.GetGenericArguments(),
                                        genericMethodArgumentsFunc());
                                }
                                catch
                                {
                                    throw new QueryCheckException(
                                        string.Format(
                                            "The query checking cannot process method: '{0}'.",
                                            QLocationExtensions.GetQLocationString(methodInfo)));
                                }
                                CheckMethod(methodInfo, resolveMember);
                                return;
                            }
                            CheckType(methodInfo, resolveField.FieldType);
                            return;
                        }
                        CheckType(methodInfo, resolveType);
                    },
                inlineType:
                    metadataToken => CheckType(
                        methodInfo,
                        module.ResolveType(
                            metadataToken,
                            methodInfo.DeclaringType.GetGenericArguments(),
                            genericMethodArgumentsFunc()))
                );
            action();
        }

        private static void CheckMethod(MethodBase methodInfo, MemberInfo resolveMember)
        {
            switch (resolveMember.MemberType)
            {
                case MemberTypes.Method:
                    CheckType(methodInfo, ((MethodInfo) resolveMember).ReturnType);
                    break;
                case MemberTypes.Constructor:
                    CheckType(methodInfo, resolveMember.DeclaringType);
                    break;
            }
        }

// ReSharper disable UnusedParameter.Local
        private static void CheckType(MethodBase methodInfo, Type type)
// ReSharper restore UnusedParameter.Local
        {
            if (typeof (IQuery).IsAssignableFrom(type))
                throw new QueryCheckException(
                    string.Format(
                        "The instance of {0} is ambiguous: '{1}'.",
                        typeof (IQuery).Name,
                        QLocationExtensions.GetQLocationString(methodInfo)));
        }

        /// <summary>
        /// http://www.codeproject.com/KB/cs/sdilreader.aspx
        /// </summary>
        private static void ProcessMethodBody(
            byte[] ilAsByteArray,
            Func<int, int, bool> inlineMethod,
            Action<int> inlineField,
            Action<int> inlineTok,
            Action<int> inlineType)
        {
            var position = 0;
            while (position < ilAsByteArray.Length)
            {
                OpCode opCode;
                ushort value = ilAsByteArray[position++];
                if (value == 0xfe)
                {
                    value = ilAsByteArray[position++];
                    opCode = multiByteOpCodes[value];
                }
                else
                    opCode = singleByteOpCodes[value];
                switch (opCode.OperandType)
                {
                    case OperandType.InlineBrTarget:
                        ReadInt32(ilAsByteArray, ref position);
                        break;
                    case OperandType.InlineField:
                        inlineField(ReadInt32(ilAsByteArray, ref position));
                        break;
                    case OperandType.InlineMethod:
                        if (inlineMethod(ReadInt32(ilAsByteArray, ref position), position)) return;
                        break;
                    case OperandType.InlineSig:
                        ReadInt32(ilAsByteArray, ref position);
                        break;
                    case OperandType.InlineTok:
                        inlineTok(ReadInt32(ilAsByteArray, ref position));
                        break;
                    case OperandType.InlineType:
                        inlineType(ReadInt32(ilAsByteArray, ref position));
                        break;
                    case OperandType.InlineI:
                        ReadInt32(ilAsByteArray, ref position);
                        break;
                    case OperandType.InlineI8:
                        ReadInt64(ref position);
                        break;
                    case OperandType.InlineNone:
                        break;
                    case OperandType.InlineR:
                        ReadDouble(ref position);
                        break;
                    case OperandType.InlineString:
                        ReadInt32(ilAsByteArray, ref position);
                        break;
                    case OperandType.InlineSwitch:
                        var count = ReadInt32(ilAsByteArray, ref position);
                        for (var i = 0; i < count; i++) ReadInt32(ilAsByteArray, ref position);
                        break;
                    case OperandType.InlineVar:
                        ReadUInt16(ref position);
                        break;
                    case OperandType.ShortInlineBrTarget:
                        ReadSByte(ref position);
                        break;
                    case OperandType.ShortInlineI:
                        ReadSByte(ref position);
                        break;
                    case OperandType.ShortInlineR:
                        ReadSingle(ref position);
                        break;
                    case OperandType.ShortInlineVar:
                        ReadByte(ref position);
                        break;
                    default:
                        throw new InvalidOperationException();
                }
            }
        }

        static QueryChecker()
        {
            singleByteOpCodes = new OpCode[0x100];
            multiByteOpCodes = new OpCode[0x100];
            foreach (var fieldInfo in typeof (OpCodes).GetFields())
            {
                if (fieldInfo.FieldType == typeof (OpCode))
                {
                    var opCode = (OpCode) fieldInfo.GetValue(null);
                    var value = (ushort) opCode.Value;
                    if (value < 0x100)
                    {
                        singleByteOpCodes[value] = opCode;
                    }
                    else
                    {
                        if ((value & 0xff00) != 0xfe00)
                        {
                            throw new ApplicationException("Invalid OpCode.");
                        }
                        multiByteOpCodes[value & 0xff] = opCode;
                    }
                }
            }
        }

        private static readonly OpCode[] multiByteOpCodes;
        private static readonly OpCode[] singleByteOpCodes;

        private static void ReadUInt16(ref int position)
        {
            position += 2;
        }

        private static int ReadInt32(byte[] bytes, ref int position)
        {
            return bytes[position++] | bytes[position++] << 8 | bytes[position++] << 0x10 | bytes[position++] << 0x18;
        }

        private static void ReadInt64(ref int position)
        {
            position += 8;
        }

        private static void ReadDouble(ref int position)
        {
            position += 8;
        }

        private static void ReadSByte(ref int position)
        {
            position++;
        }

        private static void ReadByte(ref int position)
        {
            position++;
        }

        private static void ReadSingle(ref int position)
        {
            position += 4;
        }

        public static void CheckQueries<T>(string connectionString, params Func<SqlCommand, IQuery<T>>[] queryFuncs)
        {
            CheckQueries(connectionString, QueryResultPropertyTypeChecker, queryFuncs);
        }

        public static void CheckAllQueries(string connectionString, Assembly assembly, ChoiceInvocationGetter choiceInvocationGetter, IQRepository qRepository)
        {
            CheckAllQueries(
                connectionString,
                QueryResultPropertyTypeChecker,
                (type, queryContext) => {
                    var paramType = type.GetGenericArguments().Single();
                    if (typeof (string).Equals(paramType))
                        return queryContext.CreateStringParam();
                    else if (typeof (IOption<string>).Equals(paramType))
                        return queryContext.CreateOptionalStringParam();
                    else if (typeof (Guid).Equals(paramType))
                        return queryContext.CreateGuidParam();
                    else if (typeof (IOption<Guid>).Equals(paramType))
                        return queryContext.CreateOptionalGuidParam();
                    else if (typeof(int).Equals(paramType))
                        return queryContext.CreateIntParam();
                    else if (typeof(IOption<int>).Equals(paramType))
                        return queryContext.CreateOptionalIntParam();
                    else
                        throw new InvalidOperationException();
                },
                type => true,
                memberInfo => true,
                choiceInvocationGetter,
                assembly.GetTypes(),
                qRepository);
        }

        private static QueryResultPropertyTypeChecker QueryResultPropertyTypeChecker
        {
            get
            {
                return (propertyInfo, dataRow, qLocation) => {
                           if (dataRow.GetDataTypeName().CompareSqlTypes("nvarchar"))
                               CheckPropertyType<string>(propertyInfo, dataRow, qLocation);
                           else if (dataRow.GetDataTypeName().CompareSqlTypes("uniqueidentifier"))
                               CheckPropertyType<Guid>(propertyInfo, dataRow, qLocation);
                           else if (dataRow.GetDataTypeName().CompareSqlTypes("int"))
                               CheckPropertyType<int>(propertyInfo, dataRow, qLocation);
                           else
                               throw new InvalidOperationException();
                       };
            }
        }

        private static void CheckPropertyType<T>(PropertyInfo propertyInfo, DataRow dataRow, QLocation qLocation)
        {
            var propertyType = propertyInfo.PropertyType;
            if (typeof (T).Equals(propertyType) && dataRow.GetAllowDbNull() == false)
            {
                //no-op
            }
            else
            {
                if (dataRow.GetAllowDbNull() == true && propertyType.IsClass)
                {
                    //no-op
                }

                else if (typeof (IOption<T>).Equals(propertyType) && dataRow.GetAllowDbNull() == true)
                {
                    //no-op
                }
                else
                {
                    throw new QueryCheckException(
                        string.Format(
                            @"The type of query result property was incorrect ColumnName = '{0}'. Expected SQL Server Database Engine type DataType = '{1}', AllowDBNull = '{2}' but was .NET Framework type '{3}'.{4}",
                            dataRow.GetColumnName(),
                            dataRow.GetDataTypeName(),
                            dataRow.GetAllowDbNull(),
                            propertyType,
                            qLocation.Display()));
                }
            }
        }
    }
}